﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Practices.Unity;

namespace TestByWire.Factories
{
    /// <summary>
    /// The StubInstanceFactory creates stubs of any type provided to it.
    /// Additionally, it can stub any method provided to it on an instance
    /// created with the CreateStubInstanceOf method.
    /// </summary>
    internal class StubInstanceFactory : IStubInstanceFactory
    {
        private readonly IStubCreationStrategyFactory _stubCreationStrategyFactory;
        private readonly IUnityContainer _container;

        public StubInstanceFactory(IStubCreationStrategyFactory stubCreationStrategyFactory, IUnityContainer container)
        {
            if (stubCreationStrategyFactory == null)
            {
                throw new ArgumentNullException("stubCreationStrategyFactory");
            }

            if (container == null)
            {
                throw new ArgumentNullException("container");
            }

            _stubCreationStrategyFactory = stubCreationStrategyFactory;
            _container = container;
        }

        public StubCreationResult CreateStubInstanceOf(Type typeToCreate, string instanceName)
        {
            if (typeToCreate == null)
            {
                throw new ArgumentNullException("typeToCreate");
            }

            var strategy = _stubCreationStrategyFactory.DetermineStrategy(typeToCreate, instanceName);
            object newStub = strategy.CreateInstance(typeToCreate, instanceName);

            if (newStub != null)
            {
                if(strategy.RegisterOnContainer)
                {
                    _container.RegisterInstance(typeToCreate, instanceName, newStub);
                }

                return new StubCreationResult(newStub, strategy.CompletesBuild);
            }

            throw new InvalidOperationException(@"Unable to create stub instance of type " + typeToCreate);
        }

        public Expression<Action<object>> CreateStubAction(object instanceBeingStubbed, MethodInfo methodInfo)
        {
            if (instanceBeingStubbed == null)
            {
                throw new ArgumentNullException("instanceBeingStubbed");
            }

            if (methodInfo == null)
            {
                throw new ArgumentNullException("methodInfo");
            }

            Expression<Action<object>> expression = (o) => methodInfo.Invoke(instanceBeingStubbed, GetParameters(methodInfo.GetParameters()));

            return expression;
        }

        public Expression<Action<object>> CreatePropertySetAction(object instanceBeingStubbed, MethodInfo setterMethod, object propertyValue)
        {
            if (instanceBeingStubbed == null)
            {
                throw new ArgumentNullException("instanceBeingStubbed");
            }

            if (setterMethod == null)
            {
                throw new ArgumentNullException("setterMethod");
            }

        	if (propertyValue == null)
        	{
        		throw new ArgumentNullException("propertyValue");
        	}

			Expression<Action<object>> expression = (o) => setterMethod.Invoke(instanceBeingStubbed, new[] { propertyValue });

            return expression;
        }

        public static object[] GetParameters(IEnumerable<ParameterInfo> methodParameters)
        {
            var parameters = new object[methodParameters.Count()];
            int paramIndex = 0;

            foreach (var param in methodParameters)
            {
                parameters[paramIndex++] = GetDefault(param.ParameterType);
            }

            return parameters;
        }

        public static object GetDefault(Type type)
        {
            if (type.IsValueType)
            {
                return Activator.CreateInstance(type);
            }

            return null;
        }
    }

    internal class StubCreationResult
    {
        public StubCreationResult(object instance, bool completesBuild)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }

            Instance = instance;
            CompletesBuild = completesBuild;
        }

        public object Instance { get; private set; }
        public bool CompletesBuild { get; private set; }
    }
}